#!/usr/bin/python
######
#       subscribemodule.py
#       Copyright Etelos Inc. 2007
#       Darien Hager
#       darien@etelos.com
######
import threading, os, time

class file:
	def __init__(self,name):
		self.__name = name		
		self.__parent = None
	def setParent(self,par):
		assert ((par == None) or isinstance(par,(file,folder)))
		self.__parent = par
	def getParent(self): return self.__parent
	def getName(self): return self.__name

class folder(file):
	def __init__(self,name):
		file.__init__(self,name)
		self.__children = {}
	def getChildren(self, decoration = False): 
		keys = self.__children.keys()
		if(decoration):
			ret = []
			for k in keys:
				item = self.__children[k]
				if(isinstance(item,folder)):
					ret.append(k+os.sep)
				else:
					ret.append(k)		
			return ret
		else:
			return keys
	def cleanEmpties(self):		
		# Recursive call to remove "empty" paths through the tree
		# which do not terminate in a file.		
		for c in self.__children.keys():
			child = self.__children[c]
			if(isinstance(child,folder)):
				isempty = child.cleanEmpties()
				if(isempty): self.removeChild(child)
		iamempty = (len(self.__children.keys()) == 0)		
		return iamempty
	
	def addChild(self,node):
		assert isinstance(node,(file,folder))
		parent = node.getParent()
		if(parent != None):
			if(parent == self): return # Nothing to do			
			parent.removeChild(node)		
		node.setParent(self)
		self.__children[node.getName()] = node
	def removeChild(self,node): 
		if((node.getParent() == self) and (node.getName() in self.__children.keys())):
			node.setParent(None)
			del self.__children[node.getName()]
			return True
		else:
			return False
	def getChild(self,name, autocreate = False, isFolder = True):
		try:
			return self.__children[name]
		except KeyError:
			if(autocreate):
				if(isFolder):
					n = folder(name)
				else:
					n = file(name)
				self.addChild(n)
				return n
			else:
				return None


		
				
class basicdbtype:	
	def __init__(self,filelist = None):
		if(filelist == None):
			filelist = []
			
		self.__flist = filelist			# Full names of pseudofiles		
		self.__ids = {}					# Issues or binder instances which are subscribed
										# payload of dictionary is a set of indices to the pseudofiles
										# which are subscribed, as expanded in filelist
	def getIDs(self):
		return self.__ids.keys()
		
	def clean(self,idval = None):
		if(idval != None):
			# Quickie partial cleanup
			if idval in self.__ids.keys():
				if(self.numChildren(idval) ==0):
					del self.__ids[idval]
					return True #Yes changes
			return False # No changes
			
		
		#Idval isn't set? They do a wide-spread cleaning.
			
		# First, figure out which pseudofiles on the list are no longer used 
		# anywhere, and screen them out..
										
		indexrange = range(0,len(self.__flist))
		# Make a set of indices, and later we'll remove from it		
		toremove = set(indexrange)
		
		# Prune down the set until it only contains indices which are no longer used.
		for item in self.__ids.keys(): 		# e.g. loop through instances/issues/ids
			# Clean up in case of old datastructures where lists were used instead of sets
			if(type(self.__ids[item]) == list):
				self.__ids[item] = set(self.__ids[item])
			# Check in case we can end this all early...
			if(len(toremove) == 0): 
				break # No use continuing to loop if we've already exhausted everything
				
			# Set operation to remove anything we found evidence of from our "to remove" list.			
			toremove = toremove - self.__ids[item]						
									
		if (len(toremove) > 0):
			# Ahah, we do need to do some index pruning.
			cleanedlist = self.__flist[:] # Copy
			for index in toremove:
				name = self.__flist[index]
				cleanedlist.remove(name)
		
			
			#I can reuse my old code! Let's just rename this guy here...
			filelist = cleanedlist
		
			# Make a mapping for old_index -> new_index ( mapping[old] = new )
			mapping = {}
			for i in range(0,len(self.__flist)):
				name = self.__flist[i]
				try:
					j = filelist.index(name)
					mapping[i] = j
				except ValueError:
					pass
			
			# Use that mapping on our IDs
			for instanceid in self.__ids.keys(): # For each instance or ID
				pfindices = set()	# Prepare a new value for it
				for index in self.__ids[instanceid]: # For each pseudofile in it								
					try:
						newdex = mapping[index] 
						pfindices.add(newdex)# Populate that value 
					except KeyError:
						pass # Unsubs this no-longer-existing 'index' by default
				self.__ids[instanceid] = pfindices # Set new value	
			
			# Replace our old list with the new one
			self.__flist = filelist	
		
		# Then check for IDs which have no pseudofiles, and clean those out.
		retval = False
		for idval in self.__ids.keys():
			if(self.numChildren(idval) ==0):
				del self.__ids[idval]
				retval = True
		return retval
		
	def numChildren(self,idval):
		if(idval in self.__ids.keys()):
			return len(self.__ids[idval])
		else:
			return 0
	def getChildren(self,idval):
		if(idval in self.__ids.keys()):
			indices = self.__ids[idval]
			ret = []
			for i in indices:
				ret.append(self.__flist[i])
			return ret
		else:
			return []
	def hasChild(self,idval,cname):
		if(idval in self.__ids.keys()):
			try:
				index = self.__flist.index(cname)
				return (index in self.__ids[idval])
			except ValueError:
				return False			
		else:
			return False
		
	def addChild(self,idval,cname):
		
		if(idval not in self.__ids.keys()):
			self.__ids[idval] = set()
		try:
			i = self.__flist.index(cname)
		except ValueError:
			i = len(self.__flist)
			self.__flist.append(cname)
												
		self.__ids[idval].add(i)
		return True
		
	def removeChild(self,idval,cname):
		if(idval in self.__ids.keys()):
			clist = self.__ids[idval]
			try:
				i = self.__flist.index(cname)
				clist.remove(i)
				return True
			except ValueError:
				return False			
		else:
			return False
			 

	
# Valid pathbits examples
#	("files","folder","folder","filename.txt")
#	("contacts",,"first_name.espx")
#	("issues-web","123","name.espx")
#
#
#

class subscribemodule:
	
	NORM_TYPES = ("files",) # tuple
	
	def __init__(self):
		self.__lock = threading.RLock()
		#self.__lock = lockwrapper()
		
		self.__data = {} # Core data object/root
		
		
		for kind in self.NORM_TYPES:
			# Tree structure
			self.__data[kind] = folder(kind)
			
		
		#assert set(schemas.keys()) == set(schemahashes.keys()), str(schemas.keys())+"\n\n"+str(schemahashes.keys())
		#for kind in schemas.keys():
		#	if(kind in self.__data.keys()):
		#		continue # Just added, skip
		#	self.__data[kind] = basicdbtype(schemas[kind],schemahashes[kind])			
				
	
	def __tuplizePathbits(self,pb):		
		temp = []
		if(type(pb) == tuple):
			temp = list(pb)			
		elif(type(pb) == str):
			temp = [pb,]
			return pb
		elif(type(pb) == list):
			temp = pb
		else:
			assert False
			
		# Stringify everything for consistency
		for i in range(0,len(temp)):
			temp[i] = str(temp[i])
		return tuple(temp)
	def givePicklable(self):
		self.__lock.acquire()		
		ret = (self.__data,)
		self.__lock.release()
		return ret
	def setFromPicklable(self,dump):
		self.__lock.acquire()		
		(self.__data,) = dump		
		#if(schemas != None):
		#	self.updateSchemas(schemas)			
		self.__lock.release()

	# def checkHashes(self,newhashes = None):
		# # Returns a list of resource types that need to be refreshed or supplied
		# # or else (the usual case) an empty list.
		# self.__lock.acquire()
		# self.__lastCheck = int(time.time())
		# ret = []
		# 
		# if(newhashes == None):
			# # Return all, we're doing a sanity check and not an update
			# s = set()
			# for key in self.__data.keys():
				# if(key in self.NORM_TYPES):
					# continue # Skip normal files
				# s.add(key)
			# ret = list(s)
		# else:
# 
			# new_set = set(newhashes.keys())
			# old_set = set() 		
			# for key in self.__data.keys():
				# if(key in self.NORM_TYPES):
					# continue # Skip normal files
				# old_set.add(key)
		# 
			# # This could probably be optimized a bit, set-wise
			# same = old_set.intersection(new_set)
			# removed = old_set.difference(new_set) # TODO deal with these somehow
			# added = new_set.difference(old_set)
			# 
			# ret.extend(added)
			# for key in same:
				# if(self.__data[key].hashDiffers(newhashes[key])):
					# ret.append(key)
				# 
		# self.__lock.release()
		# return ret

	# def updateSchemas(self,newschemas):
		# # Input is a dictionary: Keys are type names, payload are lists.
		# # Payload lists are of pseudofile names. 
		# # The payload lists' order must be consistent somehow!
	# 
		# self.__lock.acquire()
# 
		# for key in newschemas.keys():
			# if(key in self.__data.keys()):
				# curr = self.__data[key]
				# curr.adaptToSchema(newschemas[key])
			# else:
				# if((self.BINDER_PREFIX+"-") in key[0:(len(self.BINDER_PREFIX)+1)]):				
					# # Create a new item
					# self.__data[key] = basicdbtype("",newschemas[key]) # Blank hash is okay for now.
				# else:
					# # Not binder. 
					# assert False, "Bad schema type: "+key
# 
		# self.__lock.release()

				
	def add(self,pathbits):			
		retval = False
		pathbits = self.__tuplizePathbits(pathbits)		
		assert len(pathbits) > 1 # (kind, ...)
		self.__lock.acquire()
		kind = pathbits[0]
		if kind in self.__data.keys():			
			if(kind in self.NORM_TYPES):
				intermediate = pathbits[1:-1] # Middle parts
				endfile = pathbits[-1]	# End part			
				current = self.__data[kind]
				
				for i in intermediate:					
					current = current.getChild(i,True,True) # If not existing, autocreate folder
				c = current.getChild(endfile,True,False) # If not there, autocreate file
				assert(isinstance(c,file))
				retval = True
			else:				
				if(len(pathbits) == 3):
					curr = self.__data[kind]								
					id = pathbits[1]					
					fname = pathbits[2]
					retval = curr.addChild(id,fname)
		else:
			if(len(pathbits) == 3):
				self.__data[kind] = basicdbtype()
				retval = self.__data[kind].addChild(pathbits[1],pathbits[2])
				
		self.__lock.release()
		return retval	
	def remove(self,pathbits):
		retval = False
		pathbits = self.__tuplizePathbits(pathbits)
		if(len(pathbits) == 1): return False# Avoids attempts to unsub binder
		self.__lock.acquire()
		kind = pathbits[0]
		if kind in self.__data.keys():			
			if(kind in self.NORM_TYPES):
				current = self.__data[kind]				
				for i in pathbits[1:]:
					if(not isinstance(current,folder)):
						current = None,
						break
					current = current.getChild(i) # None if not existing
					if(current == None):						
						break
				if(current == None):
					retval = False
				else:
					parent = current.getParent()
					parent.removeChild(current)
					retval = True
			else:								
				if(len(pathbits) == 3):
					curr = self.__data[kind]
					id = pathbits[1]				
					fname = pathbits[2]
					retval = curr.removeChild(id,fname)											
		self.__lock.release()
		return retval
	def exists(self,pathbits, mustbefile = True):
		retval = False
		pathbits = self.__tuplizePathbits(pathbits)
		if(len(pathbits) < 2):
			# Lock not yet acquired, no need to release
			return False		
		self.__lock.acquire()
		kind = pathbits[0]
		if kind in self.__data.keys():		
			
			if(kind in self.NORM_TYPES):				
				intermediate = pathbits[1:-1] # Middle parts
				endfile = pathbits[-1]	# End part			
				current = self.__data[kind]				
				for i in intermediate:									
					if(current != None):
						current = current.getChild(i)									
				if(current == None):
					retval = False
				else:
					c = current.getChild(endfile) 
					if(c == None):
						retval = False
					else:
						if(mustbefile and isinstance(c,folder)):
							retval = False							
						else:
							retval = True
			else:				
				# Not necessarily a safe assertion, e.g. .DS_STORE on Mac
				# being created and checked by the subscribe module
				#assert len(pathbits) == 3, pathbits # (kind, ID, pseudofile)
				curr = self.__data[kind]
				id = pathbits[1]
				if(len(pathbits) == 3):					
					fname = pathbits[2]
					retval = curr.hasChild(id,fname)
				elif(len(pathbits) == 2):
					retval = (id in curr.getIDs())
		self.__lock.release()
		return retval	
	def listContents(self,pathbits,decoration = False):		
		retval = []
		pathbits = self.__tuplizePathbits(pathbits)		
		self.__lock.acquire()
		
		if(len(pathbits) == 0):
			# List all valid types
			ret = self.__data.keys()
			if(decoration):
				for i in range(0,len(ret)):
					ret[i] = ret[i]+os.sep
			self.__lock.release()
			return ret
		kind = pathbits[0]
		if kind in self.__data.keys():		
			
			if(kind in self.NORM_TYPES):				
				remainder = pathbits[1:]			
				current = self.__data[kind]				
				for i in remainder:									
					if(current == None): break
					current = current.getChild(i)									
				if(current != None):			
					# Decoration means whether to decorate dirs with trailing slash
					retval = current.getChildren(decoration) 
					
			else:
				curr = self.__data[kind]
				if(len(pathbits) == 1):
					# List all IDs for this category
					retval = curr.getIDs()
					if(decoration):
						retval = [a+os.sep for a in retval]					
				elif(len(pathbits) == 2):
					# Listing all pseudofiles for an ID
					
					id = pathbits[1]
					retval = curr.getChildren(id)											
				else:
					pass # Too many path bits? Do nothing (returns [] )
		elif(kind == self.BINDER_PREFIX):
			# Special case, check if it is "binder" and list binder types
			assert (len(pathbits) ==  1), "Pathbits should just be 'binder': "+str(pathbits)
			kinds = self.__data.keys()
			retval = []
			for k in kinds:
				if((self.BINDER_PREFIX+"-") in k[0:(len(self.BINDER_PREFIX)+1)]):
					if(decoration):
						retval.append(k+"/")
					else:
						retval.append(k)
		self.__lock.release()		
		return retval
	def cleanEntries(self,pathbits):	
		pathbits = self.__tuplizePathbits(pathbits)
		assert len(pathbits) > 0
		self.__lock.acquire()
		kind = pathbits[0]
		if kind in self.__data.keys():		
			
			if(kind in self.NORM_TYPES):				
				remainder = pathbits[1:]			
				current = self.__data[kind]				
				for i in remainder:														
					next = current.getChild(i)
					if((next == None) or not (instanceof(next,folder))): 
						break
					else:
						current = next				
				current.cleanEmpties()					 
			else:
				curr = self.__data[kind]
								
				if(len(pathbits) == 1): # Just type
					curr.clean()
				elif(len(pathbits) == 2): # Type and ID					
					idval = pathbits[1]
					curr.clean(idval)	
		self.__lock.release()
		return

